package cn.signalpha.modules.com.service;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.signalpha.common.cache.ConfigCache;
import cn.signalpha.common.cache.ConfigKeys;
import cn.signalpha.common.dto.ConfigDTO;
import cn.signalpha.core.log.exception.ServiceException;
import cn.signalpha.core.redis.cache.RedisCache;
import cn.signalpha.core.tool.utils.CollectionUtil;
import cn.signalpha.core.tool.utils.ObjectUtil;
import cn.signalpha.modules.com.dto.UserLevelLogDTO;
import cn.signalpha.modules.com.dto.WalletDTO;
import cn.signalpha.modules.com.entity.UserSign;
import cn.signalpha.modules.com.entity.enums.LevelSourceEnum;
import cn.signalpha.modules.com.entity.enums.UserSignTypeEnum;
import cn.signalpha.modules.com.entity.enums.WalletSourceEnum;
import cn.signalpha.modules.com.entity.enums.WalletTypeEnum;
import cn.signalpha.modules.com.mapper.UserSignMapper;
import cn.signalpha.modules.com.vo.UserSignToDayVO;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.AllArgsConstructor;
import org.springframework.data.redis.connection.BitFieldSubCommands;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;

import static cn.signalpha.common.cache.CacheNames.*;

/**
 * 服务类
 *
 * @author signalpha
 */
@Service
@AllArgsConstructor
public class UserSignService extends ServiceImpl<UserSignMapper, UserSign> {

	private final RedisCache redisCache;
	private final UserService userService;

	/**
	 * 查询签到记录
	 * @param userId 用户id
	 * @param year	年
	 * @param month	月
	 * @return
	 */
	public UserSignToDayVO selectSignIn(Long userId, Integer year, Integer month) {
		boolean signFlag = Boolean.FALSE;
		String signKey = String.format(USER_SIGN_IN, year, userId);
		LocalDate date = LocalDate.of(year, month, 1);

		//查询出一个偏移值区间的位图集合
		List<Long> list = redisCache.getValueOps().bitField(signKey,
			BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(date.lengthOfMonth())).valueAt(month * 100 + 1));

		//查询reids中当前用户补签的hash列表 (hash列表的key为补签的日期,value存在就说明这个日期补签了)
		String retroactiveKey = String.format(USER_RETROACTIVE_SIGN_IN, date.getMonthValue(), userId);
		Set<Object> keys = redisCache.hKeys(retroactiveKey);
		TreeMap<Integer, Integer> signMap = new TreeMap<>();
		if (list != null && list.size() > 0) {
			// 由低位到高位，为0表示未签，为1表示已签
			long v = list.get(0) == null ? 0 : list.get(0);
			//循环次数为当月的天数
			for (int i = date.lengthOfMonth(); i > 0; i--) {
				LocalDate d = date.withDayOfMonth(i);
				int type = 0;
				if (v >> 1 << 1 != v) {
					//状态为正常签到
					type = 1;
					//这里和当前日期对比,方便前端特殊标记今天是否签到
					if (d.compareTo(LocalDate.now()) == 0) {
						signFlag = Boolean.TRUE;
					}
				}
				if (keys.contains(d.getDayOfMonth() + "")) {
					//状态为补签
					type = 2;
				}
				//返回给前端当月的所有日期,以及签,补签或者未签的状态
				signMap.put(Integer.parseInt(d.format(DateTimeFormatter.ofPattern("dd"))), type);
				v >>= 1;
			}
		}

		UserSignToDayVO userSignToDayVO = new UserSignToDayVO();
		userSignToDayVO.setContinuousCount(getContinuousSignCount(userId));
		userSignToDayVO.setTodaySignFlag(signFlag ? 1 : 0);
		userSignToDayVO.setToday(LocalDate.now().getDayOfMonth());
		userSignToDayVO.setMonth(date.getMonthValue());
		userSignToDayVO.setSignCalendar(signMap);
		userSignToDayVO.setFirstDayOfWeek(date.getDayOfWeek().getValue());
		userSignToDayVO.setRetroactiveCount(keys.size());
		userSignToDayVO.setLengthOfLastMonth(date.minusMonths(1).lengthOfMonth());
		return userSignToDayVO;
	}

	/**
	 * 普通签到
	 */
	@Transactional(rollbackFor = Exception.class)
	public boolean saveSignIn(Long userId) {
		LocalDate localDate = LocalDate.now();
		String signKey = String.format(USER_SIGN_IN, localDate.getYear(), userId);
		long monthAndDay = Long.parseLong(localDate.format(DateTimeFormatter.ofPattern("MMdd")));

		//检测是否用户今日签到过,用getBit可以取出该用户具体日期的签到状态(位图的值只有两个,1或者0,这里1代表true)
		if(Boolean.TRUE.equals(redisCache.getValueOps().getBit(signKey, monthAndDay))){
			throw new ServiceException("今天已签到，请明日再来");
		}

		//位图的set方法会返回该位图未改变前的数值,这里如果之前没有签到过默认是0,也就是false
		boolean oldResult = Boolean.TRUE.equals(redisCache.getValueOps().setBit(signKey, monthAndDay, true));
		if(oldResult){
			throw new ServiceException("今天已签到，请明日再来");
		}

		ConfigDTO.SignInDTO config = ConfigCache.getObject(ConfigKeys.USER_SIGN, ConfigDTO.SignInDTO.class);

		//叠加签到次数
		redisCache.incr(String.format(USER_SIGN_IN_COUNT, userId));
		List<UserSign> userSignList = new LinkedList<>();
		userSignList.add(UserSign.builder()
			.createTime(LocalDateTime.now())
			.operationTime(LocalDate.now())
			.remark(UserSignTypeEnum.NORMAL.getName())
			.type(UserSignTypeEnum.NORMAL.getType())
			.ext(config.getIsExp() == 1 ? config.getExp() : 0)
			.integral(config.getIsIntegral() == 1 ? config.getIntegral() : 0d)
			.userId(userId)
			.build());

		//连续签到处理,计算出这个月该用户的到今天的连续签到天数
		int signContinuousCount = getContinuousSignCount(userId);
		doSaveContinuous(userSignList, userId, signContinuousCount);

		userSignList.forEach(userSign -> {
			baseMapper.insert(userSign);
			if(userSign.getExt() > 0){
				userService.updateUserExp(UserLevelLogDTO.builder()
					.userId(userId)
					.amount(userSign.getExt())
					.sourceId(userSign.getId().toString())
					.sourceType(LevelSourceEnum.SIGN_IN.getType())
					.remark(UserSignTypeEnum.instance(userSign.getType()).getName())
					.build());
			}
			if(userSign.getIntegral() > 0){
				userService.updateUserIntegral(WalletDTO.builder()
					.userId(userId)
					.amount(userSign.getIntegral())
					.type(WalletTypeEnum.ADD.getType())
					.sourceId(userSign.getId().toString())
					.sourceType(WalletSourceEnum.SIGN_IN.getType())
					.remark(UserSignTypeEnum.instance(userSign.getType()).getName())
					.build());
			}
		});
		return true;
	}


	/**
	 * 补签到 - 签到周期是一个月所以只需要传日的信息就可以
	 * @param userId 用户id
	 * @param day	需要补签的日期，如：7号传入7
	 * @return
	 */
	@Transactional(rollbackFor = Exception.class)
	public boolean saveSignInRetroactive(Long userId, Integer day) {
		LocalDate timeNow = LocalDate.now();

		//检测是否达上限补签
		String retroactiveKey = String.format(USER_RETROACTIVE_SIGN_IN, timeNow.getMonthValue(), userId);
		//从redis中取出用户的当月补签的集合set.我们平台的限制是三次补签
		Set<Object> keys = redisCache.hKeys(retroactiveKey);
		ConfigDTO.SignInDTO config = ConfigCache.getObject(ConfigKeys.USER_SIGN, ConfigDTO.SignInDTO.class);
		if (CollUtil.isNotEmpty(keys) && keys.size() >= config.getReplenishNum()) {
			throw new ServiceException("本月补签次数已达上限");
		}

		LocalDate retroactiveDate = LocalDate.of(timeNow.getYear(), timeNow.getMonthValue(), day);
		String signKey = String.format(USER_SIGN_IN, timeNow.getYear(), userId);
		long monthAndDay = Long.parseLong(retroactiveDate.format(DateTimeFormatter.ofPattern("MMdd")));
		if(Boolean.TRUE.equals(redisCache.getValueOps().getBit(signKey, monthAndDay)) || timeNow.getDayOfMonth() < day){
			throw new ServiceException("今日无需补签");
		}

		boolean oldResult = Boolean.TRUE.equals(redisCache.getValueOps().setBit(signKey, monthAndDay, true));
		if (oldResult) {
			throw new ServiceException("今日无需补签");
		}

		//补签记录(:月份) 过月清零,过期时间是计算出当前时间的差值,补签次数是一个月一刷新的
		redisCache.hSet(retroactiveKey, retroactiveDate.getDayOfMonth() + "", (Math.max(retroactiveDate.lengthOfMonth() - timeNow.getDayOfMonth(), 1)) * 60 * 60 * 24);
		List<UserSign> userSignList = new LinkedList<>();
		userSignList.add(UserSign.builder()
			.createTime(LocalDateTime.now())
			.operationTime(retroactiveDate)
			.remark(UserSignTypeEnum.RETROACTIVE.getName())
			.type(UserSignTypeEnum.RETROACTIVE.getType())
			.ext(config.getIsExp() == 1 ? config.getExp() : 0)
			.integral(config.getIsIntegral() == 1 ? config.getIntegral() : 0d)
			.userId(userId)
			.build());
		int signContinuousCount = getContinuousSignCount(userId);
		doSaveContinuous(userSignList, userId, signContinuousCount);

		userSignList.forEach(userSign -> {
			baseMapper.insert(userSign);
			if(userSign.getExt() > 0){
				userService.updateUserExp(UserLevelLogDTO.builder()
					.userId(userId)
					.amount(userSign.getExt())
					.sourceId(userSign.getId().toString())
					.sourceType(LevelSourceEnum.SIGN_IN.getType())
					.remark(UserSignTypeEnum.instance(userSign.getType()).getName())
					.build());
			}
			if(userSign.getIntegral() > 0){
				userService.updateUserIntegral(WalletDTO.builder()
					.userId(userId)
					.amount(userSign.getIntegral())
					.type(WalletTypeEnum.ADD.getType())
					.sourceId(userSign.getId().toString())
					.sourceType(WalletSourceEnum.SIGN_IN.getType())
					.remark(UserSignTypeEnum.instance(userSign.getType()).getName())
					.build());
			}
		});

		return true;
	}

	/**
	 * 连续签到处理
	 * @param userSignList	签到记录集合
	 * @param userId	用户id
	 * @param signContinuousCount 连续签到天数
	 */
	private void doSaveContinuous(List<UserSign> userSignList, Long userId, int signContinuousCount) {
		//连续签到奖励配置,如果是满月的情况就会有4次命中连续签到
		ConfigDTO.SignInDTO config = ConfigCache.getObject(ConfigKeys.USER_SIGN, ConfigDTO.SignInDTO.class);
		List<ConfigDTO.SignInSeriesDTO> configList = config.getSeriesList();
		if(CollectionUtil.isEmpty(config.getSeriesList())){return;}

		//初始化最大满足连续签到次数
		int times = (int) configList.stream().filter(x -> x.getDay() <= signContinuousCount).count();
		if (signContinuousCount == LocalDate.now().lengthOfMonth()){ times = 4;}
		if (times == 0){ return;}

		//取出库中连续签到的签到记录类型,周期是一个月.这样就可以查到他连续签到这个月触发了几次.
		long timesInDb = count(new LambdaQueryWrapper<UserSign>()
			.eq(UserSign::getUserId, userId).eq(UserSign::getType, UserSignTypeEnum.CONTINUOUS.getType())
			.ge(UserSign::getCreateTime, DateUtil.beginOfMonth(new Date())).le(UserSign::getCreateTime, DateUtil.endOfMonth(new Date())));

		//定义循环,次数为他连续签到次数最大命中次数
		for (int i = 0; i < times; i++) {
			//避免重复奖励，因为补签可能为中间的断层,如签到了1,2,3,4,6,7,8,这样如果补签第5天的时候,就会给用户满5天和满7天的奖励，同时满足补签和普通签到的逻辑回路
			if (timesInDb != 0) {
				timesInDb--;
				continue;
			}

			ConfigDTO.SignInSeriesDTO configuration = configList.get(i);
			if(ObjectUtil.isNotEmpty(configuration)){
				userSignList.add(UserSign.builder()
					.createTime(LocalDateTime.now())
					.remark(UserSignTypeEnum.CONTINUOUS.getName() + configuration.getDay() + "天")
					.ext(configuration.getIsExp() == 1 ? configuration.getExp() : 0)
					.integral(configuration.getIsIntegral() == 1 ? configuration.getIntegral() : 0d)
					.type(UserSignTypeEnum.CONTINUOUS.getType())
					.userId(userId)
					.build());
			}
		}
	}


	/**
	 * 获取连续签到天数
	 * @param userId 用户id
	 * @return 连续签到天数
	 */
	private int getContinuousSignCount(Long userId) {
		int signCount = 0;
		LocalDate date = LocalDate.now();
		String signKey = String.format(USER_SIGN_IN, date.getYear(), userId);
		//这里取出的是位图一个偏移值区间的值,区间起始值为当月的第一天,范围值为当月的总天数(参考命令bitfield)
		List<Long> list = redisCache.getValueOps().bitField(signKey,
			BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(date.getDayOfMonth())).valueAt(date.getMonthValue() * 100 + 1));

		if (list != null && list.size() > 0) {
			//可能该用户这个月就没有签到过,需要判断一下,如果是空就给一个默认值0
			long v = list.get(0) == null ? 0 : list.get(0);
			for (int i = 0; i < date.getDayOfMonth(); i++) {
				//如果是连续签到得到的long值右移一位再左移一位后与原始值不相等,连续天数加一
				if (v >> 1 << 1 == v) return signCount;
				signCount += 1;
				v >>= 1;
			}
		}
		return signCount;
	}
}
